iT邦幫忙

2024 iThome 鐵人賽

DAY 28
0
Modern Web

欸你是要進 Vue 了沒?系列 第 28

欸你是要進 Vue 了沒? - Day28:Vue 組件間的溝通方式之 Props、defineProps() 來自父組件の快遞請收下

  • 分享至 

  • xImage
  •  

對,是系列文!
在 Vue 中,組件之間傳遞資料的機制有許多種,本系列主要介紹父、子組件之間的溝通,分別為:「父傳子:Props」&&「子傳父:Emits」。

這兩篇都是學習使用 <script setup> 的語法糖喔,官方文件另有提供非此語法的寫法,大家可以再爬爬。

提供一下今天的小摘要:

  • Props 定義
  • defineProps() 語法
  • Props 命名格式建議
  • 靜態 vs. 動態 Props
  • 傳遞不同的值類型
    Number
    Boolean
    Array
    Object
  • 使用一個對象綁定多個 props
  • 單向數據流
  • Prop 校驗
  • Boolean 類型轉換

/images/emoticon/emoticon32.gif

我們~~開始嚕!

Props 定義

官方文件:Props 是一種特別的 attributes,你可以在組件上聲明註冊。

Props 是一種 從父組件傳遞資料給子組件 的機制。
這個機制透過屬性(attributes)來表現,而子組件能夠接收父組件傳遞的資料。

https://ithelp.ithome.com.tw/upload/images/20241012/20169139UQBD8l0qQF.png

具體的操作如下:

  1. 子組件定義 Props:可在子組件使用 defineProps() 語法來定義需要「接收資料」的屬性(props)。讓子組件明確知道自己會接收哪些資料,並且可以在組件內部使用這些 props。
  2. 父組件傳遞資料:在父組件中須 import 子組件,並在使用子組件時,將要傳遞的資料作為 props 屬性傳入,這些資料就會傳遞到子組件中。

補充

  • props 屬性可以不傳資料,除非定義了必要性(會釋出警告訊息)。
  • 一個組件中的 props 不限數量。

defineProps() 語法

設置要「接收資料」的屬性。
可以用「這個屬性是一個接口,會接住從父組件傳來的資料」來理解。

陣列做為參數

注意這邊:陣列中的元素必須為字串喔。

<script setup>
  defineProps(['你要定義的 props 屬性']);
</script>

物件作為參數

可指定類型、預設值、是否作為必要參數。

defineProps({
  你要定義的 props 屬性: 指定的類型;
})

// 定義的屬性內可以再包一個物件指定相關訊息
defineProps({
  你要定義的 props 屬性: {
  // ... 可指定類型、預設值、是否作為必要參數。
  type: String, // 指定類型
  default: 'default value', // 預設值
  required: true // 是否必要
  }
})

特性

defineProps() 語法有幾種特性可以注意:

  1. 不需要 import 就可以使用。
  2. 會 return 一個物件,其中包含定義的所有參數。
  3. 參數可以直接在模板中使用。
  4. 參數默認接受所有的數據類型。

馬上看一下子組件的使用範例:

<script setup>
// 1. 不需要 import 就可以使用。
const props = defineProps(["message"]);
// 2. 會 return 一個物件,其中包含定義的所有參數。
console.log(props);
</script>
<template>
  <h3>子組件在這邊可以接到:{{ message }}</h3>
  <!-- 3. 參數可以直接在模板中使用。 -->
</template>

看一下在瀏覽器中 2. log 出來的樣子 和 3. 模板中使用的狀況:
https://ithelp.ithome.com.tw/upload/images/20241012/20169139b40jgduEy6.png

(第 4 點我們稍後會說明~)

補充:編譯宏

defineProps() 是一種只能在 <script setup> 中使用的編譯宏,編譯宏是指在編譯器編譯時,會將傳入的參數轉為文字替換值,而不會對其做表達式的求值或類型檢查的操作。
在這邊就是將 () 中的值做了文本替換,暴露到模板,是一種預處理的操作~

Props 命名格式建議

  1. 在子組件中,以 camelCase 作為命名方式。
    可避免 props 名稱作為物件中的 key 時需要用 kebab-case 形式。
<script setup>
const props = defineProps({
  myPropsData: String // 使用 camelCase 命名方式
});
</script>

<template>
  <div>{{ myPropsData }}</div>
</template>
  1. 傳遞到 HTML 檔案中時,使用 kebab-case 命名方式,和 HTML 元素對齊。
<MyComponent my-props-data="Hihi" />

靜態 vs. 動態 Props

靜態 props 屬性範例

靜態是指:在父組件綁定時,不用 v-bind 作為綁定方式,將屬性值作為「字串」傳遞。

// child.vue

<script setup>
defineProps(["message"]);
</script>
<template>
  <h3>子組件在這邊可以接到:{{ message }}</h3>
</template>
// parent.vue

<script setup>
import Child from "./child.vue";
</script>
<template>
  <Child message="從父組件定義的資料!" />
</template>

我們沿著剛剛提到的 具體操作要點 來檢視這個範例!

  1. 子組件定義 Props:
  • defineProps(["message"]); 定義了屬性 message,而 message 屬性可以於模板中直接使用。
  1. 父組件傳遞資料:
  • import Child from "./Child.vue"; 導入子組件,並在模板中以 <Child /> 使用子組件。
  • <Child message="從父組件定義的資料!" /> 綁定 props 屬性為 message,並定義資料。

因此 message 這個 props 會將 "從父組件定義的資料!" 傳遞到子組件,子組件將這個資料顯示在模板的 <h3> 中。

瀏覽器上的呈現:
https://ithelp.ithome.com.tw/upload/images/20241012/20169139xOgOow0jko.png

用圖來理解一下~!
https://ithelp.ithome.com.tw/upload/images/20241012/20169139WV7R3hpSyo.png

動態 props 屬性範例

動態是指:在父組件綁定時,用 v-bind 作為綁定方式,將屬性值作為「表達式」傳遞。

我們把上面的例子優化成動態的綁定試試看,將父組件的傳入的資料綁定為一個「響應式狀態」。

// parentActive.vue

<script setup>
import { ref } from "vue";
import Child from "./Child.vue";

const data = ref([
  {
    id: 1,
    title: "第一個內容",
  },
  {
    id: 2,
    title: "第二個內容",
  },
  {
    id: 3,
    title: "第三個內容",
  },
]);
</script>
<template>
  <Child
    v-for="dataItem in data"
    :key="dataItem.id"
    :message="dataItem.title"
  />
</template>

這邊我們將父組件傳遞的資料定義為 data 這個響應式陣列,而在模板中,用 v-for 渲染資料、指定 key 屬性:

<template>
  <Child
    v-for="dataItem in data"
    :key="dataItem.id"
    :message="dataItem.title"
  />
</template>

看看父組件怎麼傳遞資料:
父組件 import 了子組件,並將 <Child :message="dataItem.title /> 使用 : 動態綁定 :messageprops 屬性,而屬性值為 data 中的每個物件的 title

因此 第一個內容第二個內容第一三內容 會由 message 屬性傳遞到子組件,將依序渲染,顯示在模板的 <h3> 中。

https://ithelp.ithome.com.tw/upload/images/20241012/20169139d0hQ1VMjxm.png

傳遞不同的值類型

我們剛剛有提到 defineProps() 的參數默認接受所有的數據類型。
而其可以涵蓋以下類型:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol
  • Error
  • constructor

接下來我們會以幾種參數作為範例~

註:以下類型雖然皆可以為字串形式傳遞,但記得我們要傳入綁定的是一個「表達式」,所以建議使用 : 動態綁定。

Number

傳入 Number 時,使用 : 動態綁定,因為要傳入的是表達式不是字串。

// childNumber.vue

<script setup>
defineProps(["message"]);
</script>
<template>
  <h3>子組件在這邊可以接到:{{ message }}</h3>
</template>
// parentNumber.vue

<script setup>
import { reactive } from "vue";
import Child from "./childNumber.vue";

const data = reactive({
  number: 100,
});
</script>
<template>
  <Child :message="100" />
  <Child :message="data.number" /> // 動態綁定 data 響應式物件的屬性
</template>

看看瀏覽器上呈現:
https://ithelp.ithome.com.tw/upload/images/20241012/201691394lYMshmOv4.png

Boolean

傳入布林值時,使用 : 動態綁定,因為要傳入的是表達式不是字串。

// childBoolean.vue

<script setup>
const props = defineProps(["isShow"]);
console.log(props);
</script>
<template>
  <div>我被渲染出來了:{{ isShow }}</div>
</template>
// parentBoolean.vue

<script setup>
import { ref } from "vue";
import Child from "./childBoolean.vue";

const data = ref(true);
</script>
<template>
  <Child isShow />
  <Child :isShow="true" />
  <Child :isShow="data" />
</template>

https://ithelp.ithome.com.tw/upload/images/20241012/20169139Z4jJE1txZZ.png
可以關注一下右邊,傳給子組件的 props 是 Proxy 物件(趁機打廣告)。

而在第一個子組件中,我們使用了未綁定值的 <Child isShow /> 傳遞資料,結果渲染成了空白!
這是因為:當 props 沒有指定值時,預設會被轉為 true。但!也因為在子組件中並沒有明確將 isShow 定義為 type: Boolean,到文本插值中就轉為了 '' 空字串(稍後會說明 Boolean 的 props 值轉換~)

Array

使用 : 動態綁定。

// childArray.vue

<script setup>
defineProps(["message"]);
</script>
<template>
  <div>我被渲染出來了:{{ message.join(",") }}</div>
</template>
// parentArray.vue

<script setup>
import { ref } from "vue";
import Child from "./childArray.vue";

const data = ref(["data1", "data2", "data3"]);
</script>
<template>
  <Child :message="['data1', 'data2', 'data3']" />
  <Child :message="data" />
</template>

https://ithelp.ithome.com.tw/upload/images/20241012/20169139AozsAQ2ecM.png
這邊可以先注意一下,子組件接收陣列資料,可對其做 .join() 操作,因為 join() 會 return 新的陣列,而不會改動到原資料(稍後我們會講到資料的唯讀特性)。

Object

使用 : 動態綁定。

// childObject

<script setup>
defineProps(["id", "title", "number"]);
</script>
<template>
  <div>我的 id:{{ id }}</div>
  <div>我的 title:{{ title }}</div>
  <div>我的 number:{{ number }}</div>
</template>

子組件中定義了三個屬性。

// parentObject. vue

<script setup>
import { reactive } from "vue";
import Child from "./childObject.vue";

const data = reactive({
  id: 1,
  title: "第一個內容",
  number: 100,
});
</script>
<template>
  <Child :id="data.id" :title="data.title" :number="data.number" />
</template>

這邊分別用了 :id:title:number 動態綁定 props 屬性,值對應到的是 data 這個響應式物件中的值!
https://ithelp.ithome.com.tw/upload/images/20241012/20169139v2AOijwq9j.png

使用一個對象綁定多個 prop

物件可以綁定多個 props 屬性,我們可以使用「沒有帶參數的 v-bind」 實現。

來優化一下剛剛物件的範例!

// childObject.vue

<script setup>
defineProps(["id", "title", "number"]);
</script>
<template>
  <div>我的 id:{{ id }}</div>
  <div>我的 title:{{ title }}</div>
  <div>我的 number:{{ number }}</div>
</template>
// parentObject.vue

<script setup>
import { reactive } from "vue";
import Child from "./childObject.vue";

const data = reactive({
  id: 1,
  title: "第一個內容",
  number: 100,
});
</script>
<template>
  <Child :id="data.id" :title="data.title" :number="data.number" />
  <Child v-bind="data" />
 // 這邊可以直接傳遞 data 的所有屬性到子組件
</template>

父組件我們使用了兩種寫法傳遞參數:

  1. <Child :id="data.id" :title="data.title" :number="data.number" /> 一般的 : 動態綁定。
  2. <Child v-bind="data" /> 則是使用「沒有帶參數的 v-bind」 直接傳入一個表達式,其中響應式的物件的「所有屬性」都會自動轉換為子組件的 props(只能說超讚的)。

渲染結果是一樣的哦!
https://ithelp.ithome.com.tw/upload/images/20241012/20169139WnjFNshRLv.png

單向數據流

是 props 的資料傳遞的原則!

看看官方文件怎麼說:

所有的 props 都遵循著單向綁定原則,props 因父組件的更新而變化,自然地將新的狀態向下流往子組件,而不會逆向傳遞。
這避免了子組件意外修改父組件的狀態的情況,不然應用的數據流將很容易變得混亂而難以理解。

因此 Props 的資料傳遞 會保持以下準則:

  1. 始終單向,由父組件傳遞到子組件。
  2. 父組件資料更新後,所有子組件的 props 都會更新。
  3. 父組件傳遞的資料是唯讀的,無法於子組件修改屬性的值。

我們可以來看看 3. 修改屬性的情況。
如果我們在子組件上修改:

<script setup>
defineProps(["message"]);
</script>
<template>
  <h3>子組件在這邊可以接到:{{ message++ }}</h3> 
  // 在此對父組件傳進來的資料做變更
</template>

會拋出唯讀的警吿:
https://ithelp.ithome.com.tw/upload/images/20241012/20169139b85JmQBGva.png

但我們需要修改這個值的話,有下列兩個方式:

  1. props 只用來傳入初始值,而將其傳入一個響應式狀態,再使用響應式狀態的變數去更動。

我們以這個例子來看,父組件傳入了 message 屬性,但我們將其傳入 defaultProps,並在點擊按鈕觸發後修改它的值。

<script setup>
import { ref } from "vue";

const props = defineProps(["message"]);
const defaultProps = ref(props.message);

function messageUpdate() {
  defaultProps.value = "從父組件定義的資料被修改了!";
}
</script>

<template>
  <h3>子組件在這邊可以接到:{{ defaultProps }}</h3>
  <button @click="messageUpdate">增值</button>
</template>


這樣的方式就不會去更動到 props 屬性的唯讀性。

  1. 傳入 computed 函式。
<script setup>
import { computed } from "vue";

const props = defineProps(["message"]);
const propsComputed = computed(() => {
  return "從父組件定義的資料被 computed 修改了!";
});
</script>

<template>
  <h3>子組件在這邊可以接到:{{ propsComputed }}</h3>
</template>

https://ithelp.ithome.com.tw/upload/images/20241012/20169139lk93sB1hya.png

更改對象 / 數組類型的 props

props 以物件、陣列形式傳遞時,無法修改其綁定,但是可以修改內部的值,因為傳遞的基礎是建立在記憶體指向。
但是~這樣會影響效能、並且違反了基礎的單向資料流的概念,官方提醒並不建議這樣做喔。

Prop 校驗

定義 props 時,明確定義資料的傳遞要求,可以幫助我們你同事了解傳遞這些 props 時的資料類型、預設值,以及是否必要。
注意:傳遞的資料若不符合定義,瀏覽器會拋出警告,但不會報錯無法運行哦。

設置資料類型 type

  • 單一類型
<script setup>
defineProps({
  message: Boolean,
  // 傳入的值類型設為:布林值
});
</script>
<template>
  <div>{{ message }}</div>
</template>
<script setup>
import { ref } from "vue";
import ChildType from "./childType.vue";
const messageProps = ref(true);
const messagePropsIncorrect = ref("notBoolean");
</script>
<template>
  <ChildType :message="messageProps" />
  <!-- 傳入布林值 -->
  <ChildType :message="messagePropsIncorrect" />
  <!-- 傳入字串 -->
</template>

瀏覽器拋出警告:<ChildType message="notBoolean" > 傳入的值非布林值。
https://ithelp.ithome.com.tw/upload/images/20241012/20169139jFRTyFqVS2.png

  • 多個類型
    用陣列表示。
<script setup>
defineProps({
  message: [Boolean, Number, null],
  // 傳入的值類型設為:布林值、數字、也可以是 null
});
</script>
<template>
  <div>{{ message }}</div>
</template>

設置資料必要性 required

值為:true or false。

<script setup>
defineProps({
  message: {
    type: Boolean,
    required: true,
  // 必要傳入
  },
});
</script>
<template>
  <div>子組件在這邊可以接到:{{ message }}</div>
</template>

若父組件有 傳入布林值 和 不傳入值 的情況:

<script setup>
import { ref } from "vue";
import ChildRequire from "./childRequire.vue";
const messageProps = ref(true);
</script>
<template>
  <ChildRequire :message="messageProps" />
  <!-- 傳入布林值 -->
  <ChildRequire />
  <!-- 不傳入值 -->
</template>

瀏覽器上的結果會對 <ChildRequire /> 拋出警告:
https://ithelp.ithome.com.tw/upload/images/20241012/201691390IvdapkgVm.png

設置默認值 default

默認值為原始值

以下以 Number 作為範例:

<script setup>
defineProps({
  message: {
    type: Number,
    default: 1000,
  // 設置默認值為 1000
  },
});
</script>
<template>
  <div>子組件在這邊可以接到:{{ message }}</div>
</template>
<script setup>
import { ref } from "vue";
import ChildDefault from "./childDefault.vue";
const messageProps = ref(10);
const messagePropsUndefined = ref(undefined);
const messagePropsNull = ref(null);
</script>
<template>
  <ChildDefault :message="messageProps" />
  <ChildDefault />
  <!-- 不傳入值 -->
  <ChildDefault :message="messagePropsUndefined" />
  <!-- 傳入 undefined -->
  <ChildDefault :message="messagePropsNull" />
  <!-- 傳入 null -->
</template>

瀏覽器上的呈現:

默認值為物件、陣列

以下以 物件 來作為範例,先看一下操作的語法:

default(非必要參數) {
  return 預設操作;
}
// 子組件

<script setup>
const props = defineProps({
  message: {
    type: Object,
// 會將父組件傳入的 props 資料當作參數 rawProps,並 return 一個預設值
    default(rawProps) {
      return { title: "子組件給的預設值" };
    },
  },
});
console.log(props);
</script>
<template>
  <div>子組件在這邊可以接到:{{ message.title }}</div>
</template>
// 父組件

<script setup>
import { reactive } from "vue";
import ChildDefaultObject from "./childDefaultObject.vue";
const messagePropsObject = reactive({
  message: { title: "父組件原本的資料" },
});
</script>
<template>
  <ChildDefaultObject v-bind="messagePropsObject" />
  <ChildDefaultObject />
</template>

看看瀏覽器上的呈現:
https://ithelp.ithome.com.tw/upload/images/20241012/20169139fXkceWvj1m.png
我們這邊在父組件中使用了兩個子組件,分別印出它們的 propsprops.message(props 屬性的值)檢視一下:

  • <ChildDefaultObject v-bind="messagePropsObject" />
    傳入了 message: { title: "父組件原本的資料" },是一個「帶有 message 屬性的響應式物件」,message 的值為 { title: "父組件原本的資料" },因此渲染出了 "父組件原本的資料"
  • <ChildDefaultObject />
    未傳入屬性。而印出的是一個「有 message 屬性 ... 的響應式物件」,而其中 message 的值變成了我們設置的 default 值 { title: "子組件給的預設值" },因此渲染出了 "子組件給的預設值"

默認值為函式

先看一下操作的語法:

default(非必要參數) {
  return 預設操作;
}

(又踩坑了,大家一起來看!)

// 子組件

<script setup>
const props = defineProps({
  message: {
    type: Function,
    default() {
      return "預設的函式";
    },
  },
});
console.log(`props: `, props);
console.log(`props.message: `, props.message);
</script>
<template>
  <div>子組件在這邊可以接到:{{ message }}</div>
</template>

子組件中定義了 default() { return "預設的函式"; } 其中 return 一個字串 "預設的函式"

// 父組件

<script setup>
import ChildDefaultFunction from "./childDefaultFunction.vue";
const messageFunction = function () {
  return "父組件傳來的資料我是一個函式";
};
</script>
<template>
  <ChildDefaultFunction :message="messageFunction" />
  <ChildDefaultFunction />
</template>

看看瀏覽器上呈現:
https://ithelp.ithome.com.tw/upload/images/20241012/20169139wRkSZWEx1f.png
怎麼怪怪的⋯⋯
由於!我們接到的 props 是函式本身,將其以 {{ message }} 渲染的話,呈現會是函式的字。
讓我們改為 {{ message() }} 呼叫它!
https://ithelp.ithome.com.tw/upload/images/20241012/20169139rRGbqTHNUK.png
(這樣才對麻)

自定義類型校驗函數 validator

先看一下操作的語法:

validator(父組件傳遞進來的 props 值) {
  return ["字定義預設值", "自定義預設值"].includes(父組件傳遞進來的 props 值);
}

看範例!

// 子組件

<script setup>
const props = defineProps({
  message: {
    type: String,
    validator(value) {
      // 驗證的值必須是這兩個字串之一
      return ["預設值一", "預設值二"].includes(value);
    },
  },
});
</script>
<template>
  <div>子組件在這邊可以接到:{{ message }}</div>
</template>
// 父組件

<script setup>
import ChildDefaultValidator from "./childDefaultValidator.vue";
</script>
<template>
  <ChildDefaultValidator message="不是你的預設值" />
  <ChildDefaultValidator message="預設值一" />
  <ChildDefaultValidator message="預設值二" />
</template>

父組件傳入了 不是你的預設值預設值一預設值二 這三個值,而子組件接收到時,會進入 validator 做驗證,當未符合其中定義的兩個預設值就會拋出警告。
瀏覽器上的呈現:
https://ithelp.ithome.com.tw/upload/images/20241012/20169139eFzDx7vlTx.png

值為 null

在 type 定義為 null 但沒有使用陣列語法時,會允許任何類型。

defineProps({
  message: [Boolean, Number, null],
  // 傳入的值類型設為:布林值、數字、也可以是 null
});

defineProps({
  message: null,
  // 允許任何類型
});

Boolean 類型轉換

官方文件:為了更貼近原生 boolean attributes 的行為,聲明為 Boolean 類型的 props 有特別的類型轉換規則。

未傳入值的情況

當預設值設置為 Boolean,但沒有使用 v-bind 綁定、未傳入值,會默認傳入的是 true

// 子組件

<script setup>
const props = defineProps({
  isShow: Boolean,
});
</script>
<template>
  <div v-if="isShow">子組件在這邊可以接到:{{ isShow }}</div>
</template>
// 父組件

<script setup>
import ChildDefaultBoolean from "./childDefaultBoolean.vue";
</script>
<template>
  <ChildDefaultBoolean isShow />
  // 未傳入值
</template>

瀏覽器上呈現:
https://ithelp.ithome.com.tw/upload/images/20241012/20169139SXnNkXTWMJ.png

同時設置 Boolean 與 String 為預設類型的情況

當我們同時允許 StringBoolean 時,兩者於陣列中順序前後會有不同情況。

  • BooleanString 前,默認為 ture
<script setup>
const props = defineProps({
  isShow: [Boolean, String],
});
console.log(`props.isShow: `, props.isShow);
</script>
<template>
  <div v-if="isShow">子組件在這邊可以接到:{{ isShow }}</div>
</template>

https://ithelp.ithome.com.tw/upload/images/20241012/20169139Ka7qyvFTcB.png

  • StringBoolean 前,會轉為空字串
<script setup>
const props = defineProps({
  isShow: [String, Boolean],
});
console.log(`props: `, props);
console.log(`props.isShow: `, props.isShow);
</script>
<template>
  <div v-if="isShow">子組件在這邊可以接到:{{ isShow }}</div>
</template>

https://ithelp.ithome.com.tw/upload/images/20241012/20169139yRvUQLLbSg.png

小結

呼!(必須喘口氣)

傳遞資料的概念好像不難理解,但用法和細節非常驚人⋯⋯有點期待之後會怎麼用到它的 XD
明天我們就來看看 Emits 又是怎麼傳遞的八~
(看來是沒有要饒過小腦袋)

/images/emoticon/emoticon71.gif/images/emoticon/emoticon71.gif/images/emoticon/emoticon71.gif

範例 code ⬇️

https://github.com/Jamixcs/2024iThome-jamixcs/tree/main/src/components/day28

參考資料


上一篇
欸你是要進 Vue 了沒? - Day27:Vue 組件基礎之 Component 的定義 && 使用 && 註冊
下一篇
欸你是要進 Vue 了沒? - Day29:Vue 組件間的溝通方式之 Emits、defineEmits() 子組件發出信號收到請回答 Over!
系列文
欸你是要進 Vue 了沒?30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言